import path from 'node:path';
import { getTriplitDir, loadTsModule } from './filesystem.js';
import fs from 'fs';
import { format } from 'prettier';
import {
  StringTypeOptions,
  DBSchema,
  Collection,
  Model,
  RolePermissions,
  DataType,
  TypeConfig,
  Relationship,
  isIdFilter,
  CollectionQuery,
  isDefaultFunction,
  DEFAULT_FUNCTIONS,
} from '@triplit/db';
import { blue } from 'ansis/colors';

const INDENT = '  ';

export async function readLocalSchema() {
  const triplitDir = getTriplitDir();
  const schemaPath = path.join(triplitDir, 'schema.ts');
  const result = await loadTsModule(schemaPath);
  return result && result.schema ? result.schema : null;
}

export async function writeSchemaFile(
  fileContent: string,
  options: { path?: string } = {}
) {
  const fileName = path.join(options?.path || getTriplitDir(), 'schema.ts');
  fs.mkdirSync(path.dirname(fileName), { recursive: true });
  //use prettier as a fallback for formatting
  const formatted = await format(fileContent, { parser: 'typescript' });
  fs.writeFileSync(fileName, formatted, 'utf8');
  console.log(blue(`New schema has been saved at ${fileName}`));
}

export function schemaFileContentFromSchema(schemaJSON: DBSchema) {
  const schemaContent = collectionsDefinitionToFileContent(
    schemaJSON?.collections ?? {}
  );
  const rolesContent = schemaJSON?.roles ?? {};
  const fileContent =
    `
/**
 * This file is auto-generated by the Triplit CLI.
 */ 

import { Schema as S, Roles } from '@triplit/client';
export const roles: Roles = ${JSON.stringify(rolesContent, null, 2)};
export const schema = S.Collections(${schemaContent});
      `.trim() + '\n';
  return fileContent;
}

export function collectionsDefinitionToFileContent(
  collectionsDefinition: Record<string, Collection>,
  indent = INDENT
) {
  let result = '{\n';
  for (let collectionKey in collectionsDefinition) {
    result += indent;
    result += `'${collectionKey}': {\n`;
    const {
      schema: attributes,
      permissions,
      relationships,
    } = collectionsDefinition[collectionKey];
    result += generateAttributesSection(attributes, indent + INDENT);
    result += generateRelationshipsSection(relationships, indent + INDENT);
    result += generatePermissionsSection(permissions, indent + INDENT);
    result += indent + '},\n';
  }
  return result + indent.slice(0, -2) + '}';
}

function generateAttributesSection(schema: Model, indent: string) {
  let result = '';
  result += indent + 'schema: S.Schema({\n';
  for (const path in schema.properties) {
    const itemInfo = schema.properties[path];
    result += generateAttributeSchema([path], itemInfo, indent + INDENT);
  }
  result += indent + '}),\n';
  return result;
}

function generateRelationshipsSection(
  relationships: Record<string, Relationship> | undefined,
  indent: string
) {
  let result = '';
  if (relationships) {
    result += indent + 'relationships: {\n';
    for (const relationshipName in relationships) {
      const relationship = relationships[relationshipName];
      result +=
        indent +
        `'${relationshipName}': ${generateRelationshipString(relationship)},\n`;
    }
    result += indent + '},\n';
  }
  return result;
}

function generateRelationshipString(relationship: Relationship) {
  const { query, cardinality } = relationship;
  const { collectionName, ...queryParams } = query;
  if (cardinality === 'one') {
    const { where } = query;
    const isRelationById =
      !!collectionName &&
      !!where &&
      where.length === 1 &&
      isIdFilter(where[0]) &&
      where[0][1] === '=' &&
      Object.keys(queryParams).length === 1;

    if (isRelationById)
      return `S.RelationById('${collectionName}', '${
        // @ts-expect-error
        where![0][2]
      }')`;
    else
      return `S.RelationOne('${collectionName}',${subQueryToString(
        queryParams
      )})`;
  } else {
    return `S.RelationMany('${collectionName}',${subQueryToString(
      queryParams
    )})`;
  }
}

function generatePermissionsSection(
  permissions: RolePermissions | undefined,
  indent: string
) {
  let result = '';
  if (permissions) {
    result +=
      indent +
      `permissions: ${JSON.stringify(permissions, null, 2)
        .split('\n')
        .join(`\n${indent}`)}`;
  }
  return result;
}

function generateAttributeSchema(
  path: string[],
  schemaItem: DataType,
  indent: string
) {
  if (path.length === 0) return schemaItemToString(schemaItem);
  if (path.length === 1)
    return indent + `'${path[0]}': ${schemaItemToString(schemaItem)},\n`;
  let result = '';
  const [head, ...tail] = path;
  result += indent + `'${head}': {\n`;
  result += generateAttributeSchema(tail, schemaItem, indent + INDENT);
  result += indent + '},\n';
  return result;
}

function schemaItemToString(attribute: DataType): string {
  const optional = attribute.config?.optional ?? false;
  const { type } = attribute;
  let result = '';
  switch (type) {
    case 'string':
      result = `S.String(${typeConfigToString(attribute)})`;
      break;
    case 'boolean':
      result = `S.Boolean(${typeConfigToString(attribute)})`;
      break;
    case 'number':
      result = `S.Number(${typeConfigToString(attribute)})`;
      break;
    case 'date':
      result = `S.Date(${typeConfigToString(attribute)})`;
      break;
    case 'json':
      result = `S.Json(${typeConfigToString(attribute)})`;
      break;
    case 'set':
      result = `S.Set(${schemaItemToString(
        attribute.items
      )},${typeConfigToString(attribute)})`;
      break;
    case 'record':
      result = `S.Record({${Object.entries(attribute.properties)
        .map(([key, value]) => `'${key}': ${schemaItemToString(value)}`)
        .join(',\n')}})`;
      break;
    default:
      throw new Error(`Invalid type: ${type}`);
  }
  if (optional) result = wrapOptional(result);

  return result;
}

function wrapOptional(type: string) {
  return `S.Optional(${type})`;
}

function typeConfigToString(attribute: DataType): string {
  const result: string[] = [];
  if (attribute.config?.nullable !== undefined)
    result.push(`nullable: ${attribute.config.nullable}`);
  if (attribute.config?.default !== undefined)
    result.push(`default: ${defaultValueToString(attribute.config?.default)}`);
  if (attribute.type === 'string') {
    result.push(...parseStringOptions(attribute.config));
  }
  // wrap in braces if there are options
  if (result.length) return `{${result.join(', ')}}`;
  return '';
}

function parseStringOptions(options: StringTypeOptions<any>) {
  const { enum: enumValue } = options;
  const result: string[] = [];
  if (enumValue) result.push(`enum: ${JSON.stringify(enumValue)}`);
  return result;
}

function defaultValueToString(defaultValue: TypeConfig['default']): string {
  if (isDefaultFunction(defaultValue)) {
    const { func, args } = defaultValue;
    if (!DEFAULT_FUNCTIONS.includes(func))
      throw new Error('Invalid default function name');
    const parsedArgs = args ? args.map(valueToJS).join(', ') : '';
    return `S.Default.${mapFuncIdToFuncPath(func)}(${parsedArgs})`;
  }

  return `${valueToJS(defaultValue)}`;
}

// Helpful for pulling out reserved words (ie default, return, etc)
function valueToJS(value: any) {
  if (typeof value === 'string') return `"${value}"`;
  if (typeof value === 'number') return `${value}`;
  if (typeof value === 'boolean') return `${value}`;
  if (value === null) return `null`;
  throw new Error(`Invalid value: ${value}`);
}

function subQueryToString(
  subquery: Pick<CollectionQuery, 'where' | 'limit' | 'order'>
) {
  const { where, limit, order } = subquery;
  const whereString = where ? `where: ${JSON.stringify(where)}` : '';
  const limitString = limit ? `limit: ${limit}` : '';
  const orderString = order ? `order: ${JSON.stringify(order)}` : '';
  const cleanedString = [whereString, limitString, orderString]
    .filter((str) => str)
    .join(', ');
  return `{${cleanedString}}`;
}

function mapFuncIdToFuncPath(func: (typeof DEFAULT_FUNCTIONS)[number]) {
  switch (func) {
    case 'uuidv4':
      return 'Id.uuidv4';
    case 'uuidv7':
      return 'Id.uuidv7';
    case 'uuid':
    case 'nanoid':
      return 'Id.nanoid';
    case 'now':
      return 'now';
    default:
      return func;
  }
}
